#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Author  : Leo.W
@Date    : 2024/2/21 9:33
@File    : 10-errors.py
@Software: PyCharm

[错误和异常]

Python 错误可（至少）被分为两种：
 + 语法错误(Error)：又称解析错误，编码阶段IDE工具可以识别的错误。
 + 异常(Exception)：执行时检测到的错误称为 异常。

=== 内置异常基类 ===
BaseException 是所有异常的基类,所有异常必须为一个派生自 BaseException 类。

=== BaseException 的属性 ===
 1. args ：传给异常构造器的参数元组。 某些内置异常 (例如 OSError) 接受特定数量的参数并赋予此元组中的元素特殊的含义，而其他异常通常只接受一个给出错误信息的单独字符串。
 2. __context__ ：当有旧异常已被处理的情况下又引发一个新异常的时候，新异常的 __context__ 属性会被自动设为旧异常
 3. __cause__ ：引发该异常的异常对象（raise new_err from old_err 语句中 new_err.__cause__ = old_err）,通常和__context__值相同
 4. __suppress_context__ ：表示__context__属性的异常是否已被处理 raise 语句执行后，new_err.__suppress_context__ = Frue
 5. __traceback__ ：保存关联到该异常的 回溯对象 的可写字段。 另请参阅: raise 语句。
 6. __notes__ [v3.11+]: 由此异常的注释组成的列表，它是通过 add_note() 添加的。 该属性是在调用 add_note() 时创建的。

=== BaseException 的方法 ===
add_note(note) [v3.11+]
    将字符串 note 添加到在异常字符串之后的标准回溯中显示的注释中。 如果 note 不是一个字符串则会引发 TypeError。
with_traceback(tb) [v3.11+]
    此方法会将 tb 设为新的异常回溯信息并返回异常对象。它在 PEP 3134 的异常链特性可用之前更为常用。 下面的例子演示了我们如何将一个 SomeException 实例转换为 OtherException 实例而保留回溯信息。 异常一旦被引发，当前帧会被推至 OtherException 的回溯栈顶端，就像当我们允许原始 SomeException 被传播给调用方时它的回溯栈将会发生的情形一样。:

    try:
        ...
    except SomeException:
        tb = sys.exception().__traceback__
        raise OtherException(...).with_traceback(tb)


=== 由 BaseException 派生的子类有 ===
 1. SystemExit：解释器请求退出，此异常由 sys.exit() 函数引发。。
 2. KeyboardInterrupt：用户终端执行（通常是输入）。
 3. GeneratorExit：生成器发生异常来通知退出。
 4. StandardError：所有内建标准异常的基类。
 5. Exception：是常规错误的基类,所有内置的非系统退出类异常都派生自此类。所有用户自定义异常也应当派生自此类。

承自 BaseException 以确保不会被处理 Exception 的代码（try...except）意外捕获，这样可以避免退出解释器

=== 内置异常的类层级结构如下 ===

BaseException
 ├── BaseExceptionGroup
 ├── GeneratorExit
 ├── KeyboardInterrupt
 ├── SystemExit
 └── Exception
      ├── ArithmeticError
      │    ├── FloatingPointError
      │    ├── OverflowError
      │    └── ZeroDivisionError
      ├── AssertionError
      ├── AttributeError
      ├── BufferError
      ├── EOFError
      ├── ExceptionGroup [BaseExceptionGroup]
      ├── ImportError
      │    └── ModuleNotFoundError
      ├── LookupError
      │    ├── IndexError
      │    └── KeyError
      ├── MemoryError
      ├── NameError
      │    └── UnboundLocalError
      ├── OSError
      │    ├── BlockingIOError
      │    ├── ChildProcessError
      │    ├── ConnectionError
      │    │    ├── BrokenPipeError
      │    │    ├── ConnectionAbortedError
      │    │    ├── ConnectionRefusedError
      │    │    └── ConnectionResetError
      │    ├── FileExistsError
      │    ├── FileNotFoundError
      │    ├── InterruptedError
      │    ├── IsADirectoryError
      │    ├── NotADirectoryError
      │    ├── PermissionError
      │    ├── ProcessLookupError
      │    └── TimeoutError
      ├── ReferenceError
      ├── RuntimeError
      │    ├── NotImplementedError
      │    └── RecursionError
      ├── StopAsyncIteration
      ├── StopIteration
      ├── SyntaxError
      │    └── IndentationError
      │         └── TabError
      ├── SystemError
      ├── TypeError
      ├── ValueError
      │    └── UnicodeError
      │         ├── UnicodeDecodeError
      │         ├── UnicodeEncodeError
      │         └── UnicodeTranslateError
      └── Warning
           ├── BytesWarning
           ├── DeprecationWarning
           ├── EncodingWarning
           ├── FutureWarning
           ├── ImportWarning
           ├── PendingDeprecationWarning
           ├── ResourceWarning
           ├── RuntimeWarning
           ├── SyntaxWarning
           ├── UnicodeWarning
           └── UserWarning
"""


def raise_func():
    try:
        raise NameError("exception","properties")
    except NameError as err:
        print(f"---------- {type(err)} ----------")
        print(f"__context__ = {err.__context__}") # __context__ = None
        print(f"__cause__ = {err.__cause__}")  # __cause__ = None
        print(f"__suppress_context__ = {err.__suppress_context__}") # __suppress_context__ = False
        print(f"__traceback__ = {err.__traceback__}")
        print(f"args = {err.args}")
        """ 
        raise new_exc from original_exc 语句：触发 new_exc 异常是由于另一个异常 original_exc
        如下面示例，这样做表示：
        新抛出异常 RuntimeError 是由已捕获的异常 NameError 引起
        新 RuntimeError.__context__ 和 RuntimeError.__cause__ 属性值将设置为 err
        新 RuntimeError.__suppress_context__ 属性会被设置为True，表明引发新异常的异常 err 已被处理
        
        """
        raise RuntimeError(f"caused by {type(err)}{str(err)}") from err


def print_properties():
    try:
        raise_func()
    except Exception as ex:
        print(f"---------- {type(ex)} ----------")
        # 新异常的 __context__ 和 __cause__ 属性会被自动设为已经被处理的异常。
        print(f"__context__ => {type(ex.__context__)}{ex.__context__}") # __context__ => <class 'NameError'>('exception', 'properties')
        print(f"__cause__ => {type(ex.__cause__)}{ex.__cause__}") # __cause__ => <class 'NameError'>('exception', 'properties')
        # 新异常的 __suppress_context__ 属性会被设置为True，表明引发该异常的异常已被处理
        print(f"__suppress_context__ = {ex.__suppress_context__}") # __suppress_context__ = True
        print(f"__traceback__ = {ex.__traceback__}")
        print(f"args = {ex.args}")


print_properties()

""" add_note(note) : 给异常添加注释 """


def test_add_note():
    """
    异常的 add_note(note) 方法接受一个字符串，并将其添加到异常的注释列表。
    输出结果：
    TypeError: bad type
    Add some information
    Add some more information
    """
    try:
        raise TypeError('bad type')
    except Exception as e:
        e.add_note('Add some information')
        e.add_note('Add some more information')
        raise


test_add_note()


"""
异常处理语句:
try...except 捕获异常
try...except...as 捕获异常
try...except...else 无异常时执行else内容
try...except...finally 一定会被执行
"""


def divide(x, y):
    print(f"---------- divide({x},{y}) ----------")
    try:
        result = x / y
    except ZeroDivisionError as err:
        print("division by zero!", err)
    else:
        """try ... except 语句具有可选的 else 子句，该子句必须放在所有 except 子句 之后。 它适用于 try 子句 没有引发异常但又必须要执行的代码"""
        print("result is", result)
    finally:
        print("executing finally clause")


divide(2, 1)
divide(2, 0)

"""
finally 子句触发异常情景：
 1.如果执行 try 子句期间触发了某个异常，则某个 except 子句应处理该异常。如果该异常没有 except 子句处理，在 finally 子句执行后会被重新触发。
 2.except 或 else 子句执行期间也会触发异常。 同样，该异常会在 finally 子句执行之后被重新触发。
 3.如果 finally 子句中包含 break、continue 或 return 等语句，异常将不会被重新引发。
 4.如果执行 try 语句时遇到 break,、continue 或 return 语句，则 finally 子句在执行 break、continue 或 return 语句之前执行。
 5.如果 finally 子句中包含 return 语句，则返回值来自 finally 子句的某个 return 语句的返回值，而不是来自 try 子句的 return 语句的返回值。

"""


def test_finally():
    try:
        return True
    finally:
        """finally 子句中包含 return 语句，则返回值来自 finally 的 return 语句的返回值，而不是来自 try 子句的 return 语句的返回值。"""
        return False


print(test_finally())  # False

"""
except 子句 可以用带圆括号的元组来指定多个异常:
except (RuntimeError, TypeError, NameError):
    pass
    
如果发生的异常与 except 子句中的类是同一个类或是【它的基类】时，则该类与该异常相兼容（反之则不成立 --- 列出派生类的 except 子句 与基类不兼容）。 
except 子句中应该按照从【更具体】到【更通用】的顺序列出要不获得异常类型
因此 Exception 可以被用作通配符，捕获（几乎）一切。

自定义异常：
继承 Exception 类，实现方法：
__init__(self,e,*args): 接收参数 e（可定义不同类型的多个形参）作为自定义异常的属性
__str__(self): 当触发异常时，需要异常信息。__str__方法返回自定义字符串
"""


class B(Exception):  # B 自定义异常类，继承自 Exception
    def __init__(self, value=""):
        self.value = value

    def __str__(self):
        return f"B {str(self.value)}"


class C(B):  # C 继承自 B
    def __str__(self):
        return f"C {str(self.value)}"


class D(C):  # D 继承自 C
    def __str__(self):
        return f"D {str(self.value)}"


def test_except():
    # 输出 B C D -> 可见，子类不兼容基类异常
    for cls in [B, C, D]:
        try:
            raise cls()  # 抛出异常
        except D as d:
            print(d)
        except C as c:
            print(c)
        except B as b:  # 基类放在最后匹配
            print(b)

    # 输出 B B B
    for cls in [B, C, D]:
        try:
            raise cls()
        except B as b:  # 基类放在第一个匹配,总是到这里就会捕获，不再进行后面的 except
            print(b)
        except C as c:
            print(c)
        except D as d:
            print(d)


# test_except()

"""
触发异常
raise 语句支持强制触发指定的异常
"""


def test_raise():
    try:
        raise NameError("HiThere")
    except NameError:
        print("An exception flew by!")
        """如果只想判断是否触发了异常，但并不打算处理该异常，则可以使用更简单的 raise 语句重新触发异常："""
        raise  # 抛出


# test_raise()


"""
异常链
raise...from 语句：表明一个异常是另一个异常的直接后果
raise...from None：表达禁用自动异常链
"""


def test_raise_form():
    try:
        raise ConnectionError
    except ConnectionError as exc:
        """转换异常时，这种方式很有用"""
        raise RuntimeError("Failed to open database") from exc
        # raise RuntimeError("Failed to open database") from None
    finally:
        """except 或 else 子句执行期间也会触发异常。该异常会在 finally 子句执行之后被重新触发。"""
        print("Before raise RuntimeError")


# test_raise_form()


"""
ExceptionGroup（在 3.11 版本加入）
打包了一个异常实例的列表,引发和处理多个不相关的异常
"""


def test_exception_group():
    try:
        excs = [OSError("error 1"), SystemError("error 2")]
        raise ExceptionGroup("there were problems", excs)
    except Exception as e:
        print(f"caught {type(e)}: e", e)
        raise


# test_exception_group()

"""
caught <class 'ExceptionGroup'>: e there were problems (2 sub-exceptions)
  + Exception Group Traceback (most recent call last):
  |   File "/01-basic/10-errors.py", line 257, in <module>
  |     test_exception_group()
  |   File "/01-basic/10-errors.py", line 251, in test_exception_group
  |     raise ExceptionGroup("there were problems", excs)
  | ExceptionGroup: there were problems (2 sub-exceptions)
  +-+---------------- 1 ----------------
    | OSError: error 1
    +---------------- 2 ----------------
    | SystemError: error 2
    +------------------------------------
"""


def test_nested_exception_group():
    try:
        """
        嵌套异常组
        注意：嵌套在一个异常组中的异常必须是实例，而不是类型。
        
        """
        raise ExceptionGroup(
            "group1",
            [
                OSError(1),
                SystemError(2),
                ExceptionGroup(
                    "group2",
                    [
                        OSError(3),
                        RecursionError(4)
                    ]
                )
            ]
        )
    except* OSError as e:
        """使用 except* 代替 except：可以有选择地只处理组中符合某种类型的异常"""
        print("There were OSErrors")
    except* RecursionError as e:
        print("There were RecursionError")


# test_nested_exception_group()

"""
There were OSErrors
There were RecursionError
  + Exception Group Traceback (most recent call last):
  |   File "/01-basic/10-errors.py", line 303, in <module>
  |     test_nested_exception_group()
  |   File "/01-basic/10-errors.py", line 282, in test_nested_exception_group
  |     raise ExceptionGroup(
  | ExceptionGroup: group1 (1 sub-exception)
  +-+---------------- 1 ----------------
    | SystemError: 2
    +------------------------------------
"""
